Skip to content

Fix chat runtime option matching and add direct service calls#577

Merged
hardbyte merged 6 commits intomainfrom
feat/huey-bookbot-chatflow
Feb 6, 2026
Merged

Fix chat runtime option matching and add direct service calls#577
hardbyte merged 6 commits intomainfrom
feat/huey-bookbot-chatflow

Conversation

@hardbyte
Copy link
Owner

@hardbyte hardbyte commented Feb 6, 2026

  • Store inline question options in _current_options (not just CMS-sourced), fixing cascading failures where age/reading answers stored as raw strings instead of full option dicts with typed fields like age_number
  • Add internal API handler registry for direct service-layer calls, bypassing HTTP auth for anonymous chatbot sessions (e.g. /v1/recommend)
  • Fix broken import in _find_matching_connection (app.crud.chat → chat_repo)
  • Resolve school name server-side from school_wriveted_id during session start
  • Add CEL functions for hue profile aggregation (merge, top_keys)
  • Expand seed fixtures with book catalog, themes, and flow_file loading

- Store inline question options in _current_options (not just CMS-sourced),
  fixing cascading failures where age/reading answers stored as raw strings
  instead of full option dicts with typed fields like age_number
- Add internal API handler registry for direct service-layer calls, bypassing
  HTTP auth for anonymous chatbot sessions (e.g. /v1/recommend)
- Fix broken import in _find_matching_connection (app.crud.chat → chat_repo)
- Resolve school name server-side from school_wriveted_id during session start
- Add CEL functions for hue profile aggregation (merge, top_keys)
- Expand seed fixtures with book catalog, themes, and flow_file loading
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96b19bfed0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Huey chat runtime’s option handling and introduces a direct “internal handler” pathway for certain service calls (e.g. recommendations) to avoid HTTP/auth overhead in anonymous chat sessions. It also expands seed/demo fixtures to support themes and richer catalog data, plus adds new CEL aggregation helpers.

Changes:

  • Fixes question option resolution/matching by persisting the resolved option objects into session state and improving runtime routing behavior.
  • Adds an internal handler registry for direct service-layer calls (starting with /v1/recommend) and improves variable substitution to preserve typed values.
  • Expands seed fixtures (themes, catalog fields, flow_file loading) and adds a CEL helper top_keys for hue profile aggregation.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
scripts/seed_admin_ui_data.py Adds theme seeding, flow_file merging, Airtable CSV question seeding, and new labelset fields.
scripts/fixtures/admin-ui-seed.json Expands demo fixtures (hues, reading abilities, works, themes) and references an external flow file.
docker-compose.yml Updates internal API base URL and local CORS origins.
app/tests/unit/test_cel_aggregation.py Updates custom CEL function registry expectations to include top_keys.
app/services/variable_resolver.py Enhances substitution to return raw typed values when the template is a single variable reference.
app/services/internal_api_handlers.py Introduces a registry/decorator and adds a direct handler for /v1/recommend.
app/services/chat_runtime.py Improves inline message rendering, question text substitution, option resolution/storage, and response routing.
app/services/cel_evaluator.py Adds top_keys CEL function and registers it for expression evaluation.
app/services/api_client.py Switches to structlog and makes WRIVETED_INTERNAL_API optional when building base_url.
app/services/action_processor.py Adds direct internal handler execution path for internal api_call actions and improves nested updates.
app/repositories/cms_repository.py Adjusts JSONB filter casting in random content query.
app/api/chat.py Resolves school name server-side at session start using school_wriveted_id.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +75
self.base_url = (
str(self.settings.WRIVETED_INTERNAL_API)
if self.settings.WRIVETED_INTERNAL_API
else None
)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InternalApiClient now allows base_url to be None, but initialize() still passes self.base_url into httpx.AsyncClient(base_url=...). If WRIVETED_INTERNAL_API is unset, this will fail at runtime with a low-signal error. Consider validating in init/initialize and raising a clear exception (or providing a safe default) when base_url is missing.

Copilot uses AI. Check for mistakes.
Comment on lines 420 to 425
"merge_last",
"flatten",
"collect",
"top_keys",
}
assert set(CUSTOM_CEL_FUNCTIONS.keys()) == expected_functions
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite asserts top_keys is registered, but there are no unit tests validating top_keys behavior (ordering, non-numeric values, n parameter, etc.). Adding a focused test would prevent regressions in CEL hue-profile aggregation logic.

Copilot uses AI. Check for mistakes.
Comment on lines +383 to +389
# Normalize CMS options: ensure label/value fields exist
if options:
for opt in options:
if "label" not in opt and "text" in opt:
opt["label"] = opt["text"]
if "value" not in opt and "text" in opt:
opt["value"] = opt["text"]
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The options normalization loop assumes each option is a dict and mutates it in-place. Options are sometimes a list of strings (e.g. in integration tests), which can raise at runtime, and both FlowNode.content and CMSContent.content are MutableDict-backed JSONB—mutating these objects during request handling can be flushed on the next commit (e.g. when updating session state), unintentionally persisting changes to flows/CMS content. Consider building a new normalized options list (copy) and handling string options by mapping them to {label,value} without mutating the underlying ORM JSON fields.

Suggested change
# Normalize CMS options: ensure label/value fields exist
if options:
for opt in options:
if "label" not in opt and "text" in opt:
opt["label"] = opt["text"]
if "value" not in opt and "text" in opt:
opt["value"] = opt["text"]
# Normalize CMS options: ensure label/value fields exist without mutating
# underlying ORM-backed JSON structures. Also support string options by
# mapping them to {label, value}.
if options:
normalized_options: List[Any] = []
for opt in options:
if isinstance(opt, dict):
norm_opt = dict(opt)
text_value = norm_opt.get("text")
if "label" not in norm_opt and text_value is not None:
norm_opt["label"] = text_value
if "value" not in norm_opt and text_value is not None:
norm_opt["value"] = text_value
normalized_options.append(norm_opt)
elif isinstance(opt, str):
normalized_options.append({"label": opt, "value": opt})
else:
# Preserve any unexpected option types as-is.
normalized_options.append(opt)
options = normalized_options

Copilot uses AI. Check for mistakes.
Comment on lines 1314 to 1324
# Persist available options so process_response() can match full objects
options_to_store = (
result.get("input_request", {}).get("options", [])
if awaiting_input
else []
)
state_updates = (
{"system": {"_current_options": options_to_store}}
if options_to_store
else {}
)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_current_options is only written when options_to_store is non-empty; when the next input request has no options, the previous _current_options remains in session state and can be incorrectly reused for later choice questions. Consider explicitly clearing system._current_options (e.g. set to []) when awaiting_input is false or when the next question has no options.

Suggested change
# Persist available options so process_response() can match full objects
options_to_store = (
result.get("input_request", {}).get("options", [])
if awaiting_input
else []
)
state_updates = (
{"system": {"_current_options": options_to_store}}
if options_to_store
else {}
)
# Persist available options so process_response() can match full objects.
# Always update _current_options, clearing it to [] when not awaiting input
# or when there are no options for the current question.
options_to_store = (
result.get("input_request", {}).get("options", [])
if awaiting_input
else []
)
state_updates = {"system": {"_current_options": options_to_store}}

Copilot uses AI. Check for mistakes.
db=db, wriveted_id=data.wriveted_identifier
)

limit = int(query_params.get("limit", 5))
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing limit with int(query_params.get('limit', 5)) will raise ValueError/TypeError for unexpected inputs, which will fail the whole action node. Consider validating/coercing safely (defaulting on errors) and optionally clamping to a max to avoid expensive requests.

Suggested change
limit = int(query_params.get("limit", 5))
raw_limit = query_params.get("limit", 5)
try:
limit = int(raw_limit)
except (TypeError, ValueError):
logger.warning(
"Invalid 'limit' query parameter for /v1/recommend; defaulting to 5",
limit=raw_limit,
)
limit = 5

Copilot uses AI. Check for mistakes.
Comment on lines 65 to 69
- WRIVETED_INTERNAL_API=http://api:8000
- SQLALCHEMY_WARN_20=true
- OPENAI_API_KEY=unused-test-key-for-testing
- DEBUG=true
- BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:3005","http://localhost:3006","http://localhost:3007","http://localhost:3008","http://localhost:3009","http://localhost:3010","http://localhost:8080","http://localhost:8000","http://127.0.0.1:8000"]
- BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:3001","http://localhost:3005","http://localhost:3006","http://localhost:3007","http://localhost:3008","http://localhost:3009","http://localhost:3010","http://localhost:8080","http://localhost:8000","http://127.0.0.1:8000"]
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WRIVETED_INTERNAL_API is used by CloudTasksService to build URLs like ${WRIVETED_INTERNAL_API}/internal/tasks/..., which are only mounted on the separate internal app (app/internal_api.py). Pointing this env var to the public api:8000 service will break async task processing because those routes aren't included in app/main.py. Either keep this pointing at internal:8888 in compose, or expose/mount the internal task routes on the main API and/or introduce a separate setting for public API calls.

Copilot uses AI. Check for mistakes.
…tures

- Fix repository path reference (app/crud/chat_repo.py → app/repositories/)
- Document direct service call architecture for anonymous chatbot sessions
- Document choice option matching behavior (full option objects with typed fields)
- Add top_keys CEL function to aggregation docs
- Fix user scope as read/write (writable by action nodes)
- Document flow_file key for external flow JSON in seed fixtures
- Document server-side school name resolution and theme loading in /start
- _load_flow_config returns None with warning instead of raising
  FileNotFoundError when referenced flow JSON is absent
- _ensure_theme uses (name, school_id) composite filter with .first()
  to avoid MultipleResultsFound and remove unused seed_key branch
substitute_object returns raw typed values for single {{var}} refs
(int 30, bool True) rather than stringified versions.
Covers ranking, n-parameter limiting, non-numeric value filtering,
empty/non-dict inputs, and the full merge-then-rank pipeline used
in Huey hue profile aggregation.
- Safely parse and clamp limit param in recommend handler (1-50)
- Point api service WRIVETED_INTERNAL_API at internal:8888, not itself
- Always write _current_options (even empty) to clear stale options
  from previous questions, preventing incorrect option reuse
@hardbyte hardbyte merged commit 321c23d into main Feb 6, 2026
11 checks passed
@hardbyte hardbyte deleted the feat/huey-bookbot-chatflow branch February 6, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants